Skip to content

fix(service-automation): move driver text out of engine.ts's last three store-seam log messages, with a per-seam #4632 level verdict (#6299) - #6498

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6299-engine-warn-meta
Aug 8, 2026
Merged

fix(service-automation): move driver text out of engine.ts's last three store-seam log messages, with a per-seam #4632 level verdict (#6299)#6498
os-project-manager merged 1 commit into
mainfrom
claude/issue-6299-engine-warn-meta

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6299

The last three seams of engine.ts that interpolated a foreign cause into a log
message. All three are catches around the SuspendedRunStore driver:
forgetSuspendedRun (store.delete), cancelRun (store.load) and
listSuspendedRunsDurable (store.list). Eighth instalment of the family
#5048 / #5575 / #5636 / #5661 / #5737 / #5912 / #6230.

The shared half: the message

ObjectLogger.write() adds exactly one "timestamp + level" record head per
call, so a driver error carrying newlines turns one record into several
physical lines of which only the first is greppable. The cause now goes to the
logger's structured slot via the package's existing describeThrownForLog
zero new vocabulary. Slot position follows the Logger contract:
error(message, error?, meta?) takes it third (the Error slot stays empty
per #5575, so no stack is shipped on every record), warn(message, meta?) takes
it second.

Measured on the restored concatenation, in pretty: the three-line driver error
produced 3 physical lines and serve's boot-quiet filter retained 1
and that one carried no driver fact. That is PR #6297's measurement reproduced
at these seams.

The half that is NOT shared: three separate #4632 verdicts

#6230's "the level stays warn" was deliberately not copied. Each seam was
judged on its own consequence path, and they do not agree.

1. forgetSuspendedRun — raised warn to error (durability)

this.suspendedRuns.delete(run.runId) runs on the line above the try, and
this method is the single choke point every consumption of a suspension passes
through (resume / terminal failure / cancel). So a failed store.delete()
leaves the suspension gone in-process while the durable row survives. Every
caller still reports success — resume() returns a successful result,
cancelRun() returns true, recordLog writes a terminal record — and the
surviving row is read straight back out after the next restart:
rearmSuspendedWaitTimers lists it, resume() rehydrates it through
loadSuspendedRunStrict's store read, and a continuation that has already run
runs again. Nothing retries the delete.

This is the shape the durability gate's own vocabulary already grades error
for the metadata store — deleteMetaItemFromLoader (#5259): "the surviving row
is read straight back out of storage … so the 'deleted' item reappears and
survives every restart."

2. cancelRun — raised warn to error (durability)

The failed read is turned into "no such suspended run" and the method returns
false, which its own contract documents as idempotent success (already
terminal / unknown) — so the cancellation is skipped while the call reads
clean.

The only in-repo caller measures the cost. plugin-approvals' revise-window
recall (packages/plugins/plugin-approvals/src/approval-service.ts) never reads
the boolean at all — it only catches a throw, and grades that throw error
with "cancelRun after revise-window recall failed — the run may be stranded"
(#4420). A store-read failure produces precisely that stranded run without
firing that alarm: the request is marked recalled, the record lock is
released, resumeError stays undefined, and the run stays parked in the store
to be re-armed and resumed by the next restart, inside a flow whose approval has
already been withdrawn.

Why #6230's verdict does not transfer: loadSuspendedRun is a declared
best-effort reader for incidental callers, and resumeInternal takes the strict
form exactly where the difference matters. cancelRun has no strict alternative
and its degradation decides a write.

3. listSuspendedRunsDurablestays warn (functional)

Nothing the system claims to have persisted failed to land: the rows are intact,
still resumable by id, and the next boot's rearmSuspendedWaitTimers still
re-arms them off its own store.list() — which builtin/wait-node.ts
grades error precisely because that failure breaks the promise to resume
them. This one breaks no promise, so #4632's judgment question answers no.

What is wrong here is the shape #5186 owns: an answer invented for a read
that failed, i.e. a silently short list. That rule's remedy is propagation
(rethrow, or a discriminated result), which changes this method's return
contract and is outside this issue — so the record now states the shortfall out
loud instead. Reachability is also the weakest of the three: the method has no
production consumer in-repo (only engine.test.ts) and is not on the
AutomationService spec contract, so nothing decides anything on the list
today. Raising it would alarm for the duration of an outage on a read nobody
acts on — #4632's mirror-image misuse, the trap #6230 avoided.

Why the levels need test pins

pnpm check:durability-log-level grades none of the three: its write-rule
vocabulary (DURABILITY_CRITICAL_CALLEES) does not name SuspendedRunStore's
methods, and its read-seam rule's scan roots (packages/metadata,
metadata-protocol, objectql) do not reach this package. The level pins in
the new test are the only thing holding these verdicts.

Stream side effect, stated as a side effect

ObjectLogger routes warn to stdout and error to stderr, and serve's
boot-quiet window wraps process.stdout.write alone — so raising two levels
also moves those two records off the boot buffer's drop surface. That is a
consequence of the routing, not a reason either level was raised, and it is
measured in a dedicated case so a future "tidy these back to one level" cannot
silently undo it.

Tests

New suspended-run-store-consume-log-cause.test.ts — 20 cases, real bytes off a
real ObjectLogger (a spy proves what the seam called, not what the
downstream splitter sees). Per seam: message stays single-line under a
multi-line driver error, in json and in pretty; the cause lands in the
correct argument slot; a level pin matching that seam's verdict including
which stream it lands on; behaviour unchanged; and a single-line driver renders
identically so the fix is unconditional.

Reverse verification, direction predicted before running: restoring the
concatenation turns 16 of 20 red. The 4 that stay green are the three
behaviour pins (unchanged by this PR, correctly) and — reported rather than
papered over — listSuspendedRunsDurable's level pin, which cannot go red
here because this PR does not change that seam's level. That pin guards a future
raise, not this PR's own regression; before-green/after-red was never available
for it and is not claimed.

Sweep: the issue's "last batch" premise is false

The issue closes with "治完这三处,engine.ts 这一族就全净了". It is not. The three
in scope are fixed; a content sweep of the same file found 13 more sites of
the same shape, including persistSuspendedRun 25 lines above site 1, on the
same SuspendedRunStore driver seam (store.save(), already error, message
still concatenated). Per Prime Directive #10 none is touched here — filed
separately as a finding.

Verification

  • pnpm --filter @objectstack/service-automation test — 69 files, 826 tests, all pass
  • tsc --noEmit and ESLint clean on both changed files
  • Gate list enumerated from .github/workflows/lint.yml, run one by one: all pass except check:i18n, check:i18n-coverage and check:type-check-debt, which fail identically on a pristine origin/main in this worktree (they need a full workspace build this worktree does not have) — environmental, not from this change
  • check:nul-bytes passes; a beyond-the-gate self-scan caught a raw 0x1B that the copied bootFilterRetains comment had grown, before the file was tracked and therefore before the gate could see it (the .claude/skills/** 的 markdown 不被任何门禁扫描 —— check:nul-bytes 只看 JS/TS,check:doc-authoring 的 ROOTS 不含 .claude/ #4890 shape). Removed, and the comment now names the incident.

Generated by Claude Code

…ast three store-seam log messages (#6299)

All three catches sit around the `SuspendedRunStore` driver and rendered their
failure by interpolating the thrown value's `.message` into the log MESSAGE.
`ObjectLogger.write()` adds one `<ts> <LEVEL>` head per call, so a multi-line
driver error turned ONE record into several physical lines of which only the
first was greppable. The cause now goes to the logger's structured slot via
`describeThrownForLog`, closing the #5048 / #5575 / #5636 / #5661 / #5737 /
#5912 / #6230 family for this file.

The #4632 level was judged per seam rather than batch-copied from #6230:

- forgetSuspendedRun -> raised to `error` (durability): the hot cache is
  dropped before the store delete, so a failed delete leaves the suspension
  consumed in-process and the durable row alive, to be re-listed and re-resumed
  after the next restart.
- cancelRun -> raised to `error` (durability): an unreadable store makes the
  read report "no such suspended run", so the method returns `false` (its
  contract's idempotent success) and the cancellation is silently skipped.
- listSuspendedRunsDurable -> stays `warn` (functional): nothing
  claimed-persisted failed to land; the listing degrades to the in-memory cache
  and the message now says so out loud.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USNUyHEr7uaU6MoEWXitei
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 2:28am

Request Review

@github-actions github-actions Bot added the size/l label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation.

5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via @objectstack/service-automation)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-automation)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation)
  • content/docs/releases/v9.mdx (via @objectstack/service-automation)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 8, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 8, 2026 04:06
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit b0d54bf Aug 8, 2026
25 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6299-engine-warn-meta branch August 8, 2026 04:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants